package io.kwdb.sdk;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * Represents a data point in KWDB. A point consists of measurement, tags, fields, and an optional
 * timestamp. Aligned with C++ SDK Point structure.
 */
public class Point {
  private final String measurement;
  private final Map<String, String> tags;
  private final Map<String, Object> fields; // Object can be Double, Long, Boolean, or String
  private final Long timestamp; // nanoseconds, null means use server time

  private Point(Builder builder) {
    this.measurement = builder.measurement;
    this.tags = new HashMap<>(builder.tags);
    this.fields = new HashMap<>(builder.fields);
    this.timestamp = builder.timestamp;
  }

  // Getters
  public String getMeasurement() {
    return measurement;
  }

  public Map<String, String> getTags() {
    return new HashMap<>(tags); // return copy for immutability
  }

  public Map<String, Object> getFields() {
    return new HashMap<>(fields); // return copy for immutability
  }

  public Long getTimestamp() {
    return timestamp;
  }

  public boolean hasTimestamp() {
    return timestamp != null;
  }

  // Static factory method
  public static Builder builder(String measurement) {
    return new Builder(measurement);
  }

  // Builder class
  public static class Builder {
    private final String measurement;
    private final Map<String, String> tags = new HashMap<>();
    private final Map<String, Object> fields = new HashMap<>();
    private Long timestamp = null;

    public Builder(String measurement) {
      if (measurement == null || measurement.isEmpty()) {
        throw new IllegalArgumentException("Measurement cannot be null or empty");
      }
      this.measurement = measurement;
    }

    public Builder tag(String key, String value) {
      if (key == null || key.isEmpty()) {
        throw new IllegalArgumentException("Tag key cannot be null or empty");
      }
      if (value == null) {
        throw new IllegalArgumentException("Tag value cannot be null");
      }
      tags.put(key, value);
      return this;
    }

    public Builder tags(Map<String, String> tags) {
      if (tags != null) {
        for (Map.Entry<String, String> entry : tags.entrySet()) {
          tag(entry.getKey(), entry.getValue());
        }
      }
      return this;
    }

    public Builder field(String key, double value) {
      return addField(key, value);
    }

    public Builder field(String key, long value) {
      return addField(key, value);
    }

    public Builder field(String key, int value) {
      return addField(key, (long) value);
    }

    public Builder field(String key, boolean value) {
      return addField(key, value);
    }

    public Builder field(String key, String value) {
      if (value == null) {
        throw new IllegalArgumentException("Field value cannot be null");
      }
      return addField(key, value);
    }

    private Builder addField(String key, Object value) {
      if (key == null || key.isEmpty()) {
        throw new IllegalArgumentException("Field key cannot be null or empty");
      }
      fields.put(key, value);
      return this;
    }

    public Builder fields(Map<String, Object> fields) {
      if (fields != null) {
        for (Map.Entry<String, Object> entry : fields.entrySet()) {
          addField(entry.getKey(), entry.getValue());
        }
      }
      return this;
    }

    public Builder timestamp(long timestamp) {
      if (timestamp < 0) {
        throw new IllegalArgumentException("Timestamp cannot be negative");
      }
      this.timestamp = timestamp;
      return this;
    }

    public Builder timestampNow() {
      this.timestamp = System.currentTimeMillis() * 1_000_000; // Convert to nanoseconds
      return this;
    }

    public Point build() {
      if (fields.isEmpty()) {
        throw new IllegalStateException("Point must have at least one field");
      }
      return new Point(this);
    }
  }

  /**
   * Convert point to InfluxDB Line Protocol format. Format: measurement,tag1=value1,tag2=value2
   * field1=value1,field2=value2 timestamp
   */
  public String toLineProtocol() {
    StringBuilder sb = new StringBuilder();

    // Measurement
    sb.append(escapeKey(measurement));

    // Tags (sorted for consistency)
    tags.entrySet().stream()
        .sorted(Map.Entry.comparingByKey())
        .forEach(
            entry -> {
              sb.append(',');
              sb.append(escapeKey(entry.getKey()));
              sb.append('=');
              sb.append(escapeTag(entry.getValue()));
            });

    // Space separator
    sb.append(' ');

    // Fields
    boolean first = true;
    for (Map.Entry<String, Object> entry : fields.entrySet()) {
      if (!first) {
        sb.append(',');
      }
      first = false;

      sb.append(escapeKey(entry.getKey()));
      sb.append('=');

      Object value = entry.getValue();
      if (value instanceof String) {
        sb.append('"').append(escapeString((String) value)).append('"');
      } else if (value instanceof Long || value instanceof Integer) {
        sb.append(value).append('i');
      } else if (value instanceof Boolean) {
        sb.append(value);
      } else {
        sb.append(value);
      }
    }

    // Timestamp
    if (timestamp != null) {
      sb.append(' ').append(timestamp);
    }

    return sb.toString();
  }

  private String escapeKey(String key) {
    return key.replace(" ", "\\ ").replace(",", "\\,").replace("=", "\\=");
  }

  private String escapeTag(String tag) {
    return escapeKey(tag);
  }

  private String escapeString(String str) {
    return str.replace("\\", "\\\\").replace("\"", "\\\"");
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Point point = (Point) o;
    return Objects.equals(measurement, point.measurement)
        && Objects.equals(tags, point.tags)
        && Objects.equals(fields, point.fields)
        && Objects.equals(timestamp, point.timestamp);
  }

  @Override
  public int hashCode() {
    return Objects.hash(measurement, tags, fields, timestamp);
  }

  @Override
  public String toString() {
    return "Point{"
        + "measurement='"
        + measurement
        + '\''
        + ", tags="
        + tags
        + ", fields="
        + fields
        + ", timestamp="
        + timestamp
        + '}';
  }
}
